iT邦幫忙

2024 iThome 鐵人賽

DAY 7
0
JavaScript

Signal API in Angular系列 第 7

Day 07 - ngXtension中的 toObservableSignal - 天使與魔鬼的結合體

  • 分享至 

  • xImage
  •  

我們已經介紹了toSignaltoObservable,並了解了signalsObservables如何互通以實現所需的結果。 今天,我將介紹ngXtension庫中的toObservableSignal實用函數,它結合了signalsObservables的功能。

然後,我重建了第6天的範例,應用toObservableSignal發出HTTP請求來檢索《星際大戰》字元並分別建立一個番茄計時器。

安裝 ngXtension

npm install ngxtension

從ngXtension導入到ObservableSignal

import { toObservableSignal } from 'ngxtension/to-observable-signal';

例子:

id = toObservableSignal(signal(10));
nextId = computed(() => this.id() + 1);
double = computed(() => this.id() * 2);

addOne$ = this.id.pipe(map((id) => id + 1));

<p>{{ id() }}, {{ nextId() }}, {{ double() }}</p>  // 10, 11, 20
<p>{{ addOne$ | async }}<p>  // 11

要使用toObservableSignal,我們必須從ngxtension/to-observable-signal導入。 toObservableSignal接受一個signal,它表現出signalObservable的行為。
nextId是一個唯讀signal,它將id增加1,而double則將id乘以 2。 addone$ Observable也將id增加1。

在呼叫HTTP請求之前將template-driven的表單值轉換為 Observable

在下面的範例中,我根據HTML輸入欄位中的id檢索Star War角色。 id ObservableSignal會向各種 RxJS運算子發出以檢索字元。 然後,我呼叫forkJoin來檢索該角色出現的影片。

export type Person = {
 name: string;
 height: string;
 mass: string;
 hair_color: string;
 skin_color: string;
 eye_color: string;
 gender: string;
 films: string[];
}

建立一個 Person 類型來保存 Star War 角色的實例。

const URL = 'https://swapi.dev/api/people';

export function getPerson(id: number, injector: Injector) {
 return runInInjectionContext(injector, () => {
   const http = inject(HttpClient);
   return http.get<Person>(`${URL}/${id}`).pipe(
     catchError((err) => {
       console.error(err);
       return of(undefined);
     }));
 });
}

定義getPerson函數以根據id檢索Star War角色。

export function getFilmTitle(url: string, injector: Injector): Observable<string> {
 return runInInjectionContext(injector, () => {
   const http = inject(HttpClient);
   return http.get<{ title: string }>(url)
     .pipe(
       map(({ title }) => title),
       catchError((err) => {
         console.error(err);
         return of('');
       })
     );
 });
}

定義一個getFilmTitle函數,該函數接受電影URL並呼叫Star War API來檢索電影標題。

然後,我將在組件中匯入這兩個函數來檢索要顯示的角色和電影標題。

export class App {
 id = toObservableSignal(signal(10));
 nextId = computed(() => this.id() + 1); 
 injector = inject(Injector);

 person$ = this.id.pipe(
    debounceTime(300),
    distinctUntilChanged(),
    filter((v) => v >= 1 && v <= 83),
    switchMap((v) => getPerson(v, this.injector)),
    shareReplay(1),
 );

 films$ = this.person$.pipe(
   map((p) => {
     const films = p ? p.films : [];
     return films.map((url) => getFilmTitle(url, this.injector));
   }),
   concatMap((x) => forkJoin(x)),
 );
}

id ObservableSignal將值傳送到

  • debounceTime 確保 00毫秒後沒有變化
  • distinctUntilChanged 並在新id與當前id不同時繼續
  • filter id是否在1到83之間
  • switchMap 擷取資料並取消先前未完成的Observable
  • shareReplay 快取結果,最後
  • 將結果分配給 person$ Observable

film$ Observable 從角色中提取電影URL,將getFilmTitle Observables傳遞給forkJoin來取得數值,並使用concatMap展平內部Observables。

<div>
     <label for="id">
       <span>Id: </span>
       <input id="id" name="id" type="number" min="1" max="83" [(ngModel)]="id"> 
     </label>
   </div>

   <div>
     @if(person$ | async; as person) {
       <p>Name: {{ person.name }}</p>
       <p>Height: {{ person.height }}</p>
       <p>Mass: {{ person.mass }}</p>
       <p>Hair Color: {{ person.hair_color }}</p>
       <p>Skin Color: {{ person.skin_color }}</p>
       <p>Eye Color: {{ person.eye_color }}</p>
       <p>Gender: {{ person.gender }}</p>
     } @else {
       <p>No info</p>
     }

     @if (films$ | async; as films) {
       <p>Movies</p>
       @for(film of films; track film) {
         <ul style="padding-left: 1rem;">
           <li>{{ film }}</li>
         </ul>
       }
     } @else {
       <p>No movie</p>
     }
   </div>

此範本使用async pipe解析Observables並顯示結果。

使用 ObservableSignal 導出唯讀Signal

nextId = computed(() => this.id() + 1)

nextId是一個computed signal,等於目前id加1。 然後,在範本中顯示nextId的值。

Next Id: {{ nextId() }} 

覆蓋ObservableSignal的值

ObservableSignal也是一個signl;因此,它可以呼叫set()update()來覆寫或計算signal值。

<div style="margin-bottom: 1rem;">
     <button (click)="id.set(1)">Luke Skywalker</button>
     <button (click)="id.set(4)">Darth Vader</button>
     <button (click)="id.set(5)">Princess Leia</button>
     <button (click)="id.set(14)">Han Solo</button>
     <button (click)="id.set(10)">Obi-Wan</button>
     <button (click)="id.set(20)">Yoda</button>
</div>

這些按鈕將ObservableSignal設定為不同的id,以檢索流行的星際大戰角色,例如Luke Skywalker和Darth Vader。

將Template-driven的表單值組合到Observable中以建立番茄計時器

function buildTimerString(currentSeconds: number) {
 const secondsInHour = 3600;
 const secondsInMinute = 60;
 const hours = Math.floor(currentSeconds / secondsInHour);     
 const minutes = Math.floor((currentSeconds - hours * secondsInHour) / secondsInMinute);     
 const seconds = currentSeconds - hours * secondsInHour - minutes * secondsInMinute;
 const padHours = hours < 10 ? `0${hours}` : `${hours}`;
 const padMinutes = minutes < 10 ? `0${minutes}` : `${minutes}`;
 const padSeconds = seconds < 10 ? `0${seconds}` : `${seconds}`;
 return `${padHours}:${padMinutes}:${padSeconds}`;
}

buildTimerString是一個實用程式函數,它接受整數並建立<hours>:<months>:<seconds>格式的字串。 例如,10秒錶示為"00:00:10",4000 秒錶示為"01:06:40"。

export class App {
 amount = signal(60);
 unit = signal('1');
 totalSeconds = toObservableSignal
(computed(() => this.amount() * parseInt(this.unit(), 10)));
 timer$ = this.totalSeconds.pipe(
   debounceTime(300),
   switchMap((x) => timer(0, 1000).pipe(
     map((y) => x - y),
     take(x + 1),
   )),
   shareReplay(1),
 );
 timerString$ = this.timer$.pipe(map((x) => buildTimerString(x)));
}

類似地,totalSeconds ObservableSignal將值傳送到

  • debounceTime - 等待 300 毫秒沒有新的發射
  • switchMap - 取消先前的計時器並建構一個從 x + 1 開始一直到 0 的新計時器
  • shareReplay - 快取最後的結果
  • 將結果分配給timer$ Observable

timerString$將總秒數對應到計時器字串,並將其顯示在範本中。

<div>
     <div>
       <label for="id">
         <span>Id: </span>
         <input id="id" name="id" type="number" min="1"
           [(ngModel)]="amount"> 
       </label> 
       <label for="unit">
         <span>Unit: </span>
         <select [(ngModel)]="unit">
             <option value="1">seconds</option>
             <option value="60">minutes</option>
             <option value="3600">hours</option>
         </select>
       </label>
     </div>
 </div>
 <p>{{ timerString$ | async }}</p> 

amountunit訊號double binded到ngModel,以便對HTML控制項的任何變更都會寫回它們。
當任何signal更新時,Angular都會重新計算等於總秒數的totalSeconds signal。
timer$timerString$ Observables追蹤totalSeconds signal,並在範本中顯示新的計時器字串。

鐵人賽的第七天就這樣結束了。

參考:

Stackblitz Demo:


上一篇
Day 06 - RxJS 與 Signal 互通性 第 2 部分 - toObservable
下一篇
Day 08 - 避免root-level service和 toSignal中的memory leak
系列文
Signal API in Angular10
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言